package org.unidata.mdm.rest.v1.data.service.search;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.EtalonIndexType;
import org.unidata.mdm.rest.system.service.TypedRestOutputRenderer;
import org.unidata.mdm.rest.v1.data.module.DataRestModule;
import org.unidata.mdm.rest.v1.search.converter.SearchResultToRestSearchResultConverter;
import org.unidata.mdm.rest.v1.search.ro.SearchResponseRO;
import org.unidata.mdm.rest.v1.search.ro.SearchResultRO;
import org.unidata.mdm.search.context.ComplexSearchRequestContext;
import org.unidata.mdm.search.dto.ComplexSearchResultDTO;
import org.unidata.mdm.search.dto.SearchOutputContainer;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultHitDTO;
import org.unidata.mdm.search.type.IndexType;
import org.unidata.mdm.search.util.SearchUtils;
import org.unidata.mdm.system.type.rendering.FieldDef;
import org.unidata.mdm.system.type.rendering.FragmentDef;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Data search response renderer
 *
 * @author Alexandr Serov
 * @since 07.12.2020
 **/
public class SearchDataResponseRenderer extends TypedRestOutputRenderer<SearchResponseRO, SearchOutputContainer> {

    private final SearchResultHitModifier searchResultHitModifier;

    public SearchDataResponseRenderer(SearchResultHitModifier searchResultHitModifier) {
        super(SearchResponseRO.class, SearchOutputContainer.class,
            Collections.singletonList(FieldDef.fieldDef(DataRestModule.MODULE_ID, SearchResultRO.class))
        );
        this.searchResultHitModifier = searchResultHitModifier;
    }

    @Override
    protected Map<String, Object> renderFragmentFields(FragmentDef fragmentDef, SearchOutputContainer container) {
        if (container instanceof SearchResultDTO) {
            if (!((SearchResultDTO) container).getIndexType()
                    .isOneOf(EtalonIndexType.ETALON, EntityIndexType.RECORD, EntityIndexType.RELATION)) {
                return Collections.emptyMap();
            }
        }

        if (container instanceof ComplexSearchResultDTO) {
            if (!((ComplexSearchResultDTO) container).getMain().getIndexType()
                    .isOneOf(EtalonIndexType.ETALON, EntityIndexType.RECORD, EntityIndexType.RELATION)) {
                return Collections.emptyMap();
            }
        }

        final SearchResultRO output = new SearchResultRO();
        Map<String, Object> result = new HashMap<>();
        result.put(DataRestModule.MODULE_ID, output);
        if (SearchOutputContainer.SearchOutputContainerType.COMPLEX == container.getContainerType()) {
            renderComplexSearchOutput((ComplexSearchResultDTO) container, output);

        } else if (SearchOutputContainer.SearchOutputContainerType.SIMPLE == container.getContainerType()) {
            renderSimpleSearchOutput((SearchResultDTO) container, output);
        }
        return result;
    }

    private void renderComplexSearchOutput(ComplexSearchResultDTO result, SearchResultRO output) {
        Objects.requireNonNull(result, "Result can't be null");
        Objects.requireNonNull(output, "Output can't be null");
        SearchResultDTO mainResult;
        if (ComplexSearchRequestContext.ComplexSearchRequestType.MULTI == result.getComplexSearchType()) {
            result.getSupplementary().forEach(r -> renderSimpleSearchOutput(r, output));
        } else if (Objects.nonNull(mainResult = result.getMain())) {
            Map<String, List<SearchResultHitDTO>> idsToHits = ObjectUtils.defaultIfNull(mainResult.getHitsByOriginalJoinField(), Collections.emptyMap());
            for (SearchResultDTO supplementary : result.getSupplementary()) {
                IndexType indexType = supplementary.getIndexType();
                if (indexType != null && indexType.isOneOf(EntityIndexType.RECORD, EtalonIndexType.ETALON, EntityIndexType.RELATION)) {
                    searchResultHitModifier.modifySearchResult(supplementary);
                    List<String> supFields = ObjectUtils.defaultIfNull(supplementary.getFields(), Collections.emptyList());
                    Set<String> fieldSet = new HashSet<>(supFields);
                    for (SearchResultHitDTO hit : supplementary.getHits()) {
                        String etalonId = hit.getFieldFirstValue(supplementary.getJoinField().getName());
                        List<SearchResultHitDTO> mainHits = idsToHits.get(etalonId);
                        if (CollectionUtils.isNotEmpty(mainHits)) {
                            mainHits.forEach(mainHit ->
                                mainHit.getPreview().putAll(
                                    hit.getPreview().entrySet().stream()
                                        .filter(entry -> fieldSet.contains(entry.getKey()) && !SearchUtils.isSystemField(entry.getKey())) // <- Why can't we return system fields?
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
                            );
                        }
                    }
                }
            }
        }
    }

    private void renderSimpleSearchOutput(SearchResultDTO result, SearchResultRO output) {
        Objects.requireNonNull(result, "Result can't be null");
        Objects.requireNonNull(output, "Output can't be null");
        if (result.getIndexType().isOneOf(EtalonIndexType.ETALON, EntityIndexType.RECORD, EntityIndexType.RELATION)) {
            searchResultHitModifier.modifySearchResult(result);
            SearchResultRO local = SearchResultToRestSearchResultConverter.convert(result);
            output.getHits().addAll(local.getHits());
            output.setFields(local.getFields());
            output.setHasRecords(local.isHasRecords());
            output.setMaxScore(local.getMaxScore());
            output.setTotalCount(output.getTotalCount() + local.getTotalCount());
            output.setTotalCountLimit(local.getTotalCountLimit());
            List<String> outputFields = output.getFields();
            if (outputFields == null || outputFields.isEmpty()) {
                output.setFields(local.getFields());
            } else {
                outputFields.addAll(local.getFields());
            }
        }
    }

}
